TypeScript基本数据类型、泛型、数组、断言
前置准备
TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准,其是由微软开发的自由和开源的编程语言;它的设计目标是开发大型应用,就是将 JavaScript 再封装一层,使之拥有更加完善的静态类型机制,通过编译出来的 JavaScript 运行在任何浏览器上(通过 TypeScript 编译器或 Babel 转译为 JavaScript 代码)
参考资料
TypeScript 官网 在线编辑环境 TypeScript 入门教程 菜鸟教程 TypeScript教程 TypeScript 中文文档
注:这篇文章基本照搬上面几个资料的,复制下来是因为有些地方想补充,但是缺乏上下文,所以索性直接都复制下来方便看
搭建环境
npm install -g typescript
# 检测是否安装好
tsc -v
创建一个项目
mkdir typescript-demo
npm init -y # 创建一个 package.json
tsc --init # 创建一个 tsconfig.json
touch index.html
touch hello.ts
编译 TS 文件
tsc hello.ts
因为 TS 是建立在 JS 的基础之上的,但是 NodeJS 又不能直接运行 TS 代码,实际使用是往往需要使用 tsc
将 TS 代码编译成 JS 代码。
所以一般使用的是 ts-node
这个工具
npm install -g ts-node
然后直接使用 ts-node
工具就能运行了
ts-node hello.ts
编写 Debug 的配置文件
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node", // 这里要使用这个 pwa-node 默认的 node 有点问题
"request": "launch",
"name": "TypeScript Debug",
"cwd": "${workspaceRoot}",
"sourceMaps": true,
"runtimeArgs": [
"-r",
"D:/NodeJSTools/node_global/node_modules/ts-node/register"
],
"args": [
"${relativeFile}"
]
}
],
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**" // 排除这个目录
],
}
TS 配置文件
参考资料 tsconfig.json
如果一个目录下存在一个 tsconfig.json
文件,那么它意味着这个目录是 TypeScript 项目的根目录。
使用 tsconfig.json
- 不带任何输入文件的情况下调用
tsc
,编译器会从当前目录开始去查找tsconfig.json
文件,逐级向上搜索父目录。 - 不带任何输入文件的情况下调用
tsc
,且使用命令行参数--project
(或-p)指定一个包含tsconfig.json
文件的目录。
配置详情
{
"compilerOptions": {
"module": "system",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"outDir": "./dist",
"sourceMap": true
},
"files": [
"hello.ts"
],
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}
"files"
指定一个包含相对或绝对文件路径的列表。 "include"
和 "exclude"
属性指定一个文件 glob
匹配模式列表。 支持的 glob
通配符有:
*
匹配0或多个字符(不包括目录分隔符)?
匹配一个任意字符(不包括目录分隔符)**/
递归匹配任意子目录
数据类型
参考资料 原始数据类型(大部分照搬这篇教程的,主要是这种基础向的东西也没啥好说的,默默做个笔记就行了 (~ ̄▽ ̄)~
)
JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。
原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol 和 BigInt
类型转换
let msg: any = '这是一串字符串'
let num : number = (<string>msg).length
let num2 : number = (msg as string).length
布尔值 boolean
布尔值是最基础的数据类型,在 TypeScript 中,使用 boolean
定义布尔值类型:
let isDone: boolean = false;
// 编译通过
// 后面约定,未强调编译错误的代码片段,默认为编译通过
注意,使用构造函数 Boolean 创造的对象不是布尔值:
// 注意两个 boolean 大小写不同
let createdByNewBoolean: boolean = new Boolean(1);
// Type 'Boolean' is not assignable to type 'boolean'.
// 'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.
事实上 new Boolean()
返回的是一个 Boolean 对象:(注意这里的大小写,就像 Java 一样)
let createdByNewBoolean: Boolean = new Boolean(1);
直接调用 Boolean 也可以返回一个 boolean 类型:
let createdByBoolean: boolean = Boolean(1);
在 TypeScript 中,boolean 是 JavaScript 中的基本类型,而 Boolean 是 JavaScript 中的构造函数。其他基本类型(除了 null
和 undefined
)一样,不再赘述。
数值 number
使用 number
定义数值类型:
// 其中 0b1010 和 0o744 是 ES6 中的二进制和八进制表示法,它们会被编译为十进制数字。
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;
编译结果:
var decLiteral = 6;
var hexLiteral = 0xf00d;
// ES6 中的二进制表示法
var binaryLiteral = 10;
// ES6 中的八进制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;
字符串 string
使用 string
定义字符串类型:
let myName: string = 'Tom';
let myAge: number = 25;
// 模板字符串
let sentence: string = `Hello, my name is ${myName}.
I'll be ${myAge + 1} years old next month.`;
编译结果:
var myName = 'Tom';
var myAge = 25;
// 模板字符串
var sentence = "Hello, my name is " + myName + ".\nI'll be " + (myAge + 1) + " years old next month.";
空值 void
JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void
表示没有任何返回值的函数:
function alertName(): void {
alert('My name is Tom');
}
声明一个 void
类型的变量没有什么用,因为你只能将它赋值为 undefined
和 null
:
let unusable: void = undefined;
Null 和 Undefined
在 TypeScript 中,可以使用 null
和 undefined
来定义这两个原始数据类型:
let u: undefined = undefined;
let n: null = null;
与 void
的区别是,undefined
和 null
是所有类型的子类型。也就是说 undefined
类型的变量,可以赋值给 number
类型的变量:
// 这样不会报错
let num: number = undefined;
// 这样也不会报错
let u: undefined;
let num: number = u;
而 void
类型的变量不能赋值给 number
类型的变量:
let u: void;
let num: number = u;
// Type 'void' is not assignable to type 'number'.
泛型 Any
任意值(Any)用来表示允许赋值为任意类型。
如果是一个普通类型,在赋值过程中改变类型是不被允许的:
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
但如果是 any
类型,则允许被赋值为任意类型。
let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;
这个没什么好说的,就是直接变回 JS 那样弱类型
联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
// 如果不是这两种的一个还是会报错
let myFavoriteNumber: string | number;
myFavoriteNumber = true;
// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
// Type 'boolean' is not assignable to type 'number'.
联合类型使用 |
分隔每个类型
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
function getLength(something: string | number): number {
return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
上例中,length
不是 string
和 number
的共有属性,所以会报错。
访问 string
和 number
的共有属性是没问题的:
function getString(something: string | number): string {
return something.toString();
}
元组类型
数组中元素的数据类型都一般是相同的(any[]
类型的数组可以不同),如果存储的元素数据类型不同,则需要使用元组。元组中允许存储不同类型的元素,元组可以作为参数传递给函数。
语法如下
let tuple_name = [value1,value2,value3,…value n]
使用例
let tom = ['Tom', 25];
tom.push(2) // 向元组添加元素,添加在最后面。
tom.push(3)
tom.push(4)
tom.push(5)
tom.push(6)
tom.push(7)
console.log(tom);
tom.pop() // 从元组中移除元素(最后一个),并返回移除的元素。
console.log(tom[0]); // 取得第一个元素
console.log("取得元组的长度" + tom.length)
// 更新元组元素
tom[0] = 12
tom[4] = true // 报错,因为上面只用到了两种类型 string 和 number 类型,如果要赋值 Boolean 类型必须上面也用到
也有指定类型的写法,但是这种方法就失去了上面的那种拓展性:如下定义一对值分别为 string
和 number
的元组
let tom: [string, number] = ['Tom', 25];
tom.push(2) // 依然可以添加元素,但是这里添加的元素实际是 undefined 类型
tom.push(3)
tom.push(4)
tom.push(5)
tom.push(6)
tom.push(7)
console.log(tom);
tom.pop() // 从元组中移除元素(最后一个),并返回移除的元素。
console.log(tom[0]); // 取得第一个元素
console.log("取得元组的长度" + tom.length)
// 更新元组元素(但是初始化时界定了类型的就无法更新为其它类型)
tom[0] = 12 // 报错,因为是 string 类型
tom[3] = 12 // 报错,因为初始化时只界定了两个元素,后面添加的元素实际是 undefined 类型
tom[4] = true
枚举类型
enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
let work: Days = Days.Mon // 赋值
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:(这点有点像 C#)
如果需要也可以使用手动赋值
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
// 如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的:
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true
泛型 Generics
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray<string>(3, 'x'); // ['x', 'x', 'x']
定义泛型的时候,可以一次定义多个类型参数:
function swap<T, U>(tuple: [T, U]): [U, T] {
console.log(typeof tuple[0]);
console.log(typeof tuple[1]);
return [tuple[1], tuple[0]];
}
swap([7, 'seven']);
// number
// string
// ['seven', 7]
泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
上例中,我们使用了 extends
约束了泛型 T 必须符合接口 Lengthwise
的形状,也就是必须包含 length
属性。
多个类型参数之间也可以互相约束:(例如这里的 其中要求 T
继承 U
)
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 });
泛型接口
interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>;
}
// 此时在使用泛型接口的时候,需要定义泛型的类型。
let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, add: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = add;
}
}
let myGenericNumber = new GenericNumber<number>(18, (x, y) => {
return x + y
});
数组的类型
在 TypeScript 中,数组类型有多种定义方式,比较灵活。
经典表示法
最简单的方法是使用「类型 + 方括号」来表示数组:
let fibonacci: number[] = [1, 1, 2, 3, 5];
数组的项中不允许出现其他的类型:
let fibonacci: number[] = [1, '1', 2, 3, 5];
// Type 'string' is not assignable to type 'number'.
数组的一些方法的参数也会根据数组在定义时约定的类型进行限制:
let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push('8');
// Argument of type '"8"' is not assignable to parameter of type 'number'.
上例中,push 方法只允许传入 number 类型的参数,但是却传了一个 "8" 类型的参数,所以报错了。这里 "8" 是一个字符串字面量类型,会在后续章节中详细介绍。
数组泛型
我们也可以使用数组泛型(Array Generic)来表示数组:
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
any 在数组中的应用
一个比较常见的做法是,用 any 表示数组中允许出现任意类型:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型。
值 as 类型
// 或
<类型>值
不过类型的形式还可能表示一个泛型,所以最好使用 值 as 类型
类型断言的常见用途有以下几种:
1、将一个联合类型断言为其中一个类型(当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,只能访问此联合类型的所有类型中共有的属性或方法)
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
// 需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
原理就是 (animal as Fish).swim()
这段代码隐藏了 animal
可能为 Cat
的情况,将 animal
直接断言为 Fish
了,而 TypeScript 编译器信任了我们的断言,故在调用 swim()
时没有编译错误。
可是 swim
函数接受的参数是 Cat | Fish
,一旦传入的参数是 Cat
类型的变量,由于 Cat
上没有 swim
方法,就会导致运行时错误了。
总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
2、将一个父类断言为更加具体的子类
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
3、将任何一个类型断言为 any
const foo: number = 1;
window.foo = 1; // 这时会报错,所以需要将其断言成 any
(window as any).foo = 1;
4、将 any 断言为一个具体的类型
举例来说,历史遗留的代码中有个 getCacheData
,它的返回值是 any
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();